#!/usr/bin/python
# -*- encoding: UTF-8 -*-

from __future__ import print_function

from decimal import Decimal
import decimal
import datetime
import threading
import os
import sys
import time

from gtkmvc import Model

class TablaDeDemandas:
    '''
        Permite configurar y recuperar la demanda de segun dia,hora,piso.
          - dia: lunes a domingo.
          - hora: 0..23
          - piso: 0..9
    '''
    def __init__(self):
        # Para que no sea hardcodeado deberia leerse de un archivo esta tabla.
        basica = ['B','B','B','B','B','B','M','B','B','B','B']
        d08a10 = ['A','B','B','B','B','B','B','B','B','B','B']
        d10a17 = ['M','M','M','M','M','M','M','M','M','M','M']
        d17a19 = ['B','A','A','A','A','A','A','A','A','A','A']

        demandaLV = dict()
        for hora in xrange(24):
            demandaLV[hora] = basica
            if hora in [8,9]:
                demandaLV[hora] = d08a10
            if hora in [10, 11, 12, 13, 14, 15, 16]:
                demandaLV[hora] = d10a17
            if hora in [17, 18]:
                demandaLV[hora] = d17a19
        demandaSD = dict()
        for hora in xrange(24):
            demandaSD[hora] = basica

        self.tabla = {}
        for dia in ["lunes", "martes", "miercoles", "jueves", "viernes"]:
            self.tabla[dia] = demandaLV
        for dia in ["sabado","domingo"]:
            self.tabla[dia] = demandaSD

    def _set_demanda(self, dia, hora, piso, demanda):
        '''
            Metodo de utilidad para simplificar la lectura de un archivo de la tabla.
            Nota: Para que sea util demandaLV/demandaSD deben tener una
             instancia distinta de list en cada hora.
        '''
        if demanda not in ['B', 'M', 'A']:
            raise AttributeError("La demanda debe ser 'A', 'B' o 'M'.")
        self.tabla[dia][hora][piso] = demanda

    def get_demanda(self, dia, hora, piso):
        ''' Recuperar la demanda para un determinado (dia, hora, piso).'''
        return self.tabla[dia][hora][piso]

    def get_pisos_con_demanda(self, dia, hora, demanda):
        ''' Armar un conjunto con los pisos de demanda dada por parametro. '''
        if demanda not in ['B', 'M', 'A']:
            raise AttributeError("La demanda debe ser 'A', 'B' o 'M'.")

        resultado = set()
        demandas = self.tabla[dia][hora]
        for piso,d in enumerate(demandas):
            if d == demanda:
                resultado.add(piso)

        return resultado

class Ascensor (Model, threading.Thread):
    '''Simula la vida de un ascensor.'''
    
    # Estado
    posicion = 0
    nombre = ""
    velocidad = Decimal(0.0)
    peso = 0
    capacidad = 0
    movimiento = "detenido"
    destino = None
    pedidos = list()
    puerta_abierta = 4
    ventilacion = False
    luces = False
    tope = 0
    __observables__ = [ "posicion", "velocidad", "peso", "capacidad", "ventilacion", "luces", "movimiento", "destino", "pedidos", "puerta_abierta" ]

    def is_moviendose(self):
        return self.movimiento in ["subiendo", "bajando"]

    def inicializar(self, nombre, velocidad, capacidad, posicion = 0, tope = 0):
        self.nombre = nombre
        self.posicion = posicion
        self.velocidad = Decimal(velocidad)
        self.peso = Decimal(0.0)
        self.capacidad = Decimal(capacidad)
        self.movimiento = "detenido"
        self.tope = tope
        self.destino = None
        self.puerta_abierta = 4
        self.ventilacion = False
        self.luces = False

        self.lock = threading.RLock()

        vel = self.velocidad.quantize(Decimal(".01"), rounding=decimal.ROUND_DOWN)
        cap = self.capacidad.quantize(Decimal(".01"), rounding=decimal.ROUND_DOWN)
        print ("\tVelocidad {0} pisos/segundo".format(str(vel)))
        print ("\tCapacidad {0} kilogramos".format(str(cap)))

    def __init__(self):
        Model.__init__(self)
        threading.Thread.__init__(self)

        # Conjunto de peticiones en espera de ser atendidas.
        self.pedidos = list()

        # Administracion del hilo
        self.detener = False
    
        print ("Ascensor creado.")

    def __setitem__(self, key, item):
        self.lock.acquire()
        super(Ascensor,self).__setitem__(key,item)
        self.lock.release()

    def __getitem__(self, key, item):
        self.lock.acquire()
        super(Ascensor,self).__getitem__(key,item)
        self.lock.release()

    # Para mostrar mejor el ascensor en __str__
    _mov_str = {"subiendo":"▲", "bajando":"▼", "detenido":"◼", "inhabilitado":"⟁"} #⚠⬣⬛⬜
    def __str__(self):
        return "Ascensor {0} (mov.:{1}, posicion:{2}, dest.:{3}, detener:{4}, peso={5})".format(self.nombre, self._mov_str[self.movimiento], self.posicion, self.destino, self.detener, self.peso)

    def subir(self, pesable):
        self.peso += pesable.peso

    def bajar(self, pesable):
        self.peso -= pesable.peso

    def run(self):
        pasos = 0

        muestreo = Decimal(0.1) # Cada cuantos segundos actualizo el estado del ascensor.
        delta = self.velocidad * muestreo
        delta = delta.quantize(Decimal(".01"), rounding=decimal.ROUND_DOWN)

        print ("{0}: Ascensor activo.".format(self.nombre))
        while (not self.detener):

            ultimo = None
            time.sleep( muestreo )
            if self.is_moviendose():
                while self.puerta_abierta > 0 and not self.detener:
                    self.puerta_abierta -= 1
                    time.sleep(5*muestreo)
                if self.detener:
                    break

                if self.destino != None:
                    distancia = Decimal(self.destino - self.posicion)
                else:
                    distancia = 0

                if distancia > 0:
                    self.posicion += delta
                    if self.posicion > self.destino:
                        self.posicion = self.destino
                elif distancia < 0:
                    self.posicion -= delta
                    if self.posicion < self.destino:
                        self.posicion = self.destino

                if self.posicion == int(self.posicion):
                    print (self)
                    
                if self.posicion == self.destino:
                    self.movimiento = "detenido"
                    print (self)
                    while self.puerta_abierta < 4 and not self.detener:
                        self.puerta_abierta += 1
                        time.sleep(5*muestreo)

                    esperas = 0
                    while self.puerta_abierta == 4 and not self.detener:
                        time.sleep(5*muestreo)
                        if self.peso <= self.capacidad:
                            esperas += 1
                        if esperas >= 6:
                            if self.peso >= 0:
                                while self.puerta_abierta > 0:
                                    self.puerta_abierta -= 1
                                    time.sleep(5*muestreo)

                    while self.peso > self.capacidad and not self.detener:
                        print ("{0}: Ascensor con mucho peso. Detenido.".format(self.nombre))
                        time.sleep(30*muestreo)
                
                    self.pedidos.remove(self.destino)
                    self.destino = None
                    self.indicar_destino()

            else: # if self.is_moviendose()
                if len(self.pedidos) == 1:
                    self.indicar_destino()
                    print (self.destino, self.posicion, self.pedidos, self.peso)
#                if len(self.pedidos) == 1 and self.destino == self.posicion and self.peso == 0:
#                    self.pedidos.pop()
        print ("{0}: Ascensor terminado.".format(self.nombre))
                          
    def indicar_destino(self, destino = None):
        self.lock.acquire()
        aceptado = False
        if not destino is None:
            if destino not in self.pedidos:
                # Regla de aceptacion de demanda del ascensor.
                if self.movimiento == "detenido":
                    aceptado = True
                else:
                    peso_ok = self.peso + 75 < self.capacidad
                    if destino > self.posicion and self.movimiento == "subiendo":
                        if peso_ok and int(self.posicion)+2 > destino:
                            aceptado = True
                    elif destino < self.posicion and self.movimiento == "bajando":
                        if peso_ok and int(self.posicion)-2 < destino:
                            aceptado = True
                if aceptado:
                    if destino not in self.pedidos:
                        self.pedidos.append( int(destino) )

        if self.destino == None:
            if self.pedidos:
                mas_cercano = None
                distancia = sys.maxint
                for d in self.pedidos:
                    if abs(d-self.posicion) < distancia:
                        mas_cercano = d
                        distancia = abs(d-self.posicion)

#                self.destino = self.pedidos.remove(mas_cercano)
                self.destino = mas_cercano
                if self.destino > self.posicion:
                    self.movimiento = "subiendo"
                if self.destino < self.posicion:
                    self.movimiento = "bajando"
            else:
                self.movimiento = "detenido"
        self.lock.release()
        return aceptado

    def terminar(self):
        self.detener = True

class Edificio(Model, threading.Thread):
    pedidos = list()
    nombre = None
    pisos = None

    __observables__ = [ "pedidos" ]

    def __init__(self, nombre, pisos):
        Model.__init__(self)
        threading.Thread.__init__(self)
    
        self.lock = threading.RLock()

        # Estado
        self.nombre = nombre
        self.pisos = pisos
        self.pedidos = list()
        self.asignados = dict()
        self.demandas = TablaDeDemandas()

        # Los ascensores funcionan aparte.
        self.ascensores = list()

        # Lista de personas. Para el metodo terminar.
        self.personas = list()

        # Administracion del hilo.
        self.detener = False
        print ("Edificio creado.")

    def agregar_ascensor(self, ascensor):
        self.ascensores.append(ascensor)

    def registrar_persona(self, persona):
        self.personas.append(persona)
        
    def _asignar_ascensor(self, ascensor, destino):
        '''
            Metodo para que utilice plc para indicar la asignacion de ascensores.
        '''
        asignado = False
        if ascensor in self.ascensores:
            if not destino in self.asignados:
                asignado = ascensor.indicar_destino(destino)
                if asignado:
                    self.asignados[destino] = ascensor
        return asignado

    def llamar_ascensor(self, destino):
        print ("{0}: Llamando ascensor desde el piso {1}".format(self.nombre, destino))

        if destino < 0 or destino >= self.pisos:
            print ("{0}: El edificio no tiene piso {1}".format(self.nombre, destino))
            return None
        
        # Agrego el ascensor a los pedidos.
        self.pedidos.append(destino)
        print ("{0}: Pedidos {1}".format(self.nombre, self.pedidos))
            
    def run(self):
        print ("{0}: Activando ascensores.".format(self.nombre))
        for a in self.ascensores:
                a.start()

        segundos = 0
        while (not self.detener):
            time.sleep(1)
            segundos += 1
            if self.pedidos:
                p = self.pedidos.pop(-1)
                self.pedidos.append(p)

            if segundos % 10 != 0:
                continue
            dia = 'lunes'
            hora = 9
            if False:
                dia = datetime.datetime.now().strftime("%a")
                hora = datetime.datetime.now().hour
            print ("{0}: Analizando demanda para el dia {1} a la hora {2}.".format(self.nombre,dia,hora))
            # Obtengo los pisos criDestinoticos de la tabla.
            #criticos = self.demandas.get_pisos_con_demanda(dia,hora,'A')
            criticos = set([0,10])
            print ("{0}: Criticos: {1}.".format(self.nombre,criticos))
            # Conjunto de ascensores que ya estan en un piso critico.
            ascensores_ok = set()
            for a in self.ascensores:
                if self.detener:
                    break
                # Si el ascensor esta libre.
                if not a.pedidos and not a.is_moviendose():
                    if a.posicion == int(a.posicion):
                        try:
                            criticos.remove(int(a.posicion))
                            ascensores_ok.add(a)
                        except KeyError:
                            pass

            for a in self.ascensores:
                if self.detener:
                    break
                if a not in ascensores_ok:
                    # Si el ascensor esta libre.
                    if not a.pedidos and not a.is_moviendose():
#                        print ("{0}: {1} ".format(self.nombre,a.nombre))
                        distancia = self.pisos
                        elegido = None
                        for c in criticos:
#                            print ("{0}: {1} ".format(self.nombre,abs(a.posicion - c)))
                        
                            if abs(a.posicion - c) < distancia:
                                distancia = abs(a.posicion - c)
                                elegido = c

                        if not elegido is None and not a.pedidos:
                            print ("{0}: Enviando ascensor {1} al piso critico {2}.".format(self.nombre, a.nombre, c))
                        
                            if not a.indicar_destino(c):
                                print ("{0}: El ascensor rechaza el destino critico.".format(self.nombre))
                    
            
        print ("{0}: Edificio terminado.".format(self.nombre))

    def terminar(self):
        self.detener = True
        for a in self.ascensores:
            a.terminar()

        for p in self.personas:
            p.terminar()
    pass

class Persona(threading.Thread):# , Model):
    def __init__(self, nombre, edificio, inicio, destino, peso, delay=0):
#        Model.__init__(self)
        threading.Thread.__init__(self)

        # Estado
        self.nombre = nombre
        self.edificio = edificio
        self.inicio = int(inicio) # En que piso la persona pide el ascensor.
        self.peso = int(peso) # Cuanto pesa esta persona.
        self.destino = int(destino)  # A que piso quiere ir la persona.
        self.ubicacion = None # En que piso esta la persona.
        self.ascensor = None # En que ascensor esta viajando.
        self.delay = delay

        self.detener = False

#        self.__observables__ = [ "destino", "ubicacion", "ascensor" ]

    def run(self):
        self.ubicacion = self.inicio

        if self.delay > 0:
            time.sleep(self.delay)

        print ("{0}: esta en {1} y se dispone a pedir ascensor para ir a {2}.".format(self.nombre, self.ubicacion, self.destino))

        while self.ascensor is None and not self.detener:
            self.edificio.llamar_ascensor(self.inicio)
            while self.ascensor is None and not self.detener:
                try:
                    self.ascensor = self.edificio.asignados[self.ubicacion]
                except KeyError:
                    print ("{0}: Esperando un ascensor.".format(self.nombre))
                    time.sleep(0.5)

        if not self.ascensor is None:
            if not self.detener:
                print ("{0}: Esperando al ascensor {1}.".format(self.nombre, self.ascensor.nombre))

            while self.ascensor.posicion != self.inicio and not self.detener:
                time.sleep(0.1)

            if not self.detener:
                print ("{0}: subir al ascensor.".format(self.nombre))
                while self.ascensor.puerta_abierta < 4 and not self.detener:
                    time.sleep(0.5)
                self.ascensor.subir(self)
                if self.ascensor.peso > self.ascensor.capacidad:
                    time.sleep(2)
                    print ("{0}: El ascensor esta lleno me voy a casa.".format(self.nombre))
                else:
                    self.ascensor.indicar_destino(self.destino)
                    while self.ascensor.puerta_abierta > 0 and not self.detener:
                        self.ascensor.puerta_abierta -= 1
                        time.sleep(0.1)
                    while self.ascensor.posicion != self.destino and not self.detener:
                        time.sleep(1)
                        print ("{0}: en ascensor {1} en piso {2}, llendo a {3}.".format(self.nombre, self.ascensor.nombre, self.ascensor.posicion, self.destino))
                    if self.ascensor.posicion == self.destino:
                        print ("{0}: En destino.".format(self.nombre))

                while self.ascensor.puerta_abierta < 4 and not self.detener:
                    time.sleep(0.5)
                self.ascensor.bajar(self)

        print ("{0}: Persona Terminado.".format(self.nombre))

    def terminar(self):
        self.detener = True
        
if __name__ == '__main__':
    pass

